/*
  This is a updated versoin (v2) based on log-simulater.
  There are two most important new designs:
  1. FIFO Write adopts APPEND_ONLY, instead of in-place updates.
  2. When the block choosing to evict out of FIFO is a "old version",
     then drop it off，won't activate write-back, and move head pointer to deal with next one.
*/
#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <memory.h>

#include "config.h"
#include "zbd-cache.h"
#include "hashtb_pb.h"
#include "simulator_alpha.h"

#define OFF_BAND_TMP_PERSISIT   0 // The head 80MB of FIFO for temp persistence band needed to clean.
#define OFF_PB                80*1024*1024
#define OFF_BAND_REGION         20*1024*1000*1000 // 20GB

#define DISK_READ(fd,buf,size,offset) (size)
#define DISK_WRITE(fd,buf,size,offset) (size)


static FIFOCtrl global_fifo_ctrl;
static FIFODesc* fifo_desp_array;
static char* BandBuffer;
static uint64_t NSMRBands = 266600;		// smr band cnt = 266600; almost 8TB data size.
static uint64_t BNDSZ = 40*1000*1024;      // bandsize =40MB  (20MB~40MB)


int ACCESS_FLAG = 1;

static uint64_t	band_size_num;
static uint64_t	num_each_size;

static FIFODesc* getFIFODesp();
// static void* smr_fifo_monitor_thread();
static void flushFIFO();

static long simu_read_smr_bands;
static long simu_flush_bands;
static long simu_flush_band_size;

static long simu_n_collect_fifo;
static long simu_n_read_fifo;
static long simu_n_write_fifo;
static long simu_n_read_smr;

static long simu_n_fifo_write_HIT = 0;

static int simu_RMWs = 0;

static int invalidDespInFIFO(FIFODesc* desp);
#define isFIFOEmpty (global_fifo_ctrl.head == global_fifo_ctrl.tail)
#define isFIFOFull  ((global_fifo_ctrl.tail + 1) % (NBLOCK_SMR_PB + 1) == global_fifo_ctrl.head)

static unsigned long getBandSize(uint64_t offset);
static uint64_t getBandOffset(uint64_t blk_off);


/*
 * init inner ssd buffer hash table, strategy_control, buffer, work_mem
 */
int InitDMSimulator()
{
    /* initialliz related constants */
    band_size_num = (BNDSZ / 1024000) / 2 + 1;
    num_each_size = NSMRBands / band_size_num;

    global_fifo_ctrl.n_used = 0;
    global_fifo_ctrl.head = global_fifo_ctrl.tail = 0;

    posix_memalign((void**) &fifo_desp_array, 1024,sizeof(FIFODesc) * (NBLOCK_SMR_PB + 1));


    FIFODesc* fifo_hdr = fifo_desp_array;
    long i;
    for (i = 0; i < (NBLOCK_SMR_PB + 1); fifo_hdr++, i++)
    {
        fifo_hdr->despId = i;
        fifo_hdr->isValid = 0;
    }

    posix_memalign((void**) &BandBuffer, 512, sizeof(char) * BNDSZ * 50);


    /** AIO related **/

    /** statistic related **/
    simu_read_smr_bands = 0;
    simu_flush_bands = 0;
    simu_flush_band_size = 0;

    simu_n_collect_fifo = 0;
    simu_n_read_fifo = 0;
    simu_n_write_fifo = 0;
    simu_n_read_smr = 0;

    initSSDTable(NBLOCK_SMR_PB + 1);
    return 0;
    
}

int
simu_dmsmr_read(char *buffer, size_t size, uint64_t offset)
{
    DespTag		tag;
    FIFODesc    *ssd_hdr;
    size_t		i;
    int	        returnCode;
    long		ssd_hash;
    long		despId;

    for (i = 0; i * BLKSIZE < size; i++)
    {
        tag.offset = offset + i * BLKSIZE;
        ssd_hash = ssdtableHashcode(tag);
        despId = ssdtableLookup(tag, ssd_hash);

        if (despId >= 0)
        {
            /* read from fifo */
            simu_n_read_fifo++;
            ssd_hdr = fifo_desp_array + despId;

            returnCode = DISK_READ(smr_fd, buffer, BLKSIZE, ssd_hdr->despId * BLKSIZE + OFF_PB);
        }
        else
        {
            /* read from actual smr */
            simu_n_read_smr++;
            returnCode = DISK_READ(smr_fd, buffer, BLKSIZE, offset + i * BLKSIZE + OFF_BAND_REGION);

        }
    }
    ACCESS_FLAG = 1;
    return 0;
}

int
simu_dmsmr_write(char *buffer, size_t size, uint64_t offset)
{
    DespTag		tag;
    FIFODesc        *ssd_hdr;
    size_t		i;
    int		returnCode = 0;
    long		ssd_hash;

    for (i = 0; i * BLKSIZE < size; i++)
    {
        tag.offset = offset + i * BLKSIZE;

        /* APPEND_ONLY */
        ssd_hdr = getFIFODesp();
        ssd_hdr->tag = tag;

        /* Update HashTable and Descriptor array */
        ssd_hash = ssdtableHashcode(tag);
        long old_despId = ssdtableUpdate(tag, ssd_hash, ssd_hdr->despId);
        if(old_despId>=0){
          FIFODesc* oldDesp = fifo_desp_array + old_despId;
          invalidDespInFIFO(oldDesp); ///invalid the old desp
        }

        returnCode = DISK_WRITE(smr_fd, buffer, BLKSIZE, ssd_hdr->despId * BLKSIZE + OFF_PB);
        simu_n_write_fifo ++;
    }
    ACCESS_FLAG = 1;

    return size;
}

static int
invalidDespInFIFO(FIFODesc* desp)
{
    desp->isValid = 0;
    global_fifo_ctrl.n_used--;
    int isHeadChanged = 0;
    while(!fifo_desp_array[global_fifo_ctrl.head].isValid && !isFIFOEmpty)
    {
        global_fifo_ctrl.head = (global_fifo_ctrl.head + 1) % (NBLOCK_SMR_PB + 1);
        isHeadChanged = 1;
    }
    return isHeadChanged;
}

static FIFODesc *
getFIFODesp()
{
    FIFODesc* newDesp;
    if(isFIFOFull)
    {
        /* Log structure array is full fill */
        flushFIFO();
    }

    /* Append to tail */
    newDesp = fifo_desp_array + global_fifo_ctrl.tail;
    newDesp->isValid = 1;
    global_fifo_ctrl.tail = (global_fifo_ctrl.tail + 1) % (NBLOCK_SMR_PB + 1);

    return newDesp;
}

/** Persistent Buffer write-back dirty blocks using RMW Model: Read->Modify->Write. **/
static void
flushFIFO()
{
    if(global_fifo_ctrl.head == global_fifo_ctrl.tail) // Log structure array is empty.
        return;

    int     returnCode;
    long    dirty_n_inBand = 0;
    double  wtrAmp;

    FIFODesc* leader = fifo_desp_array + global_fifo_ctrl.head;

    /* Create a band-sized buffer for readind and flushing whole band bytes */
    uint64_t		thisBandSize = getBandSize(leader->tag.offset);
    uint64_t       thisBandOff = getBandOffset(leader->tag.offset);

    /** R **/
    /* read whole band from smr to buffer*/
    returnCode = DISK_READ(smr_fd, BandBuffer, thisBandSize, thisBandOff + OFF_BAND_REGION);
    simu_read_smr_bands++;

    /** M **/
    /* Combine cached pages from FIFO which are belong to the same band */


    long curPos = leader->despId;
    while(curPos != global_fifo_ctrl.tail)
    {
        FIFODesc* curDesp = fifo_desp_array + curPos;
        long nextPos = (curDesp->despId + 1) % (NBLOCK_SMR_PB + 1);

        /* If the block belongs the same band with the header of fifo. */
        if (curDesp->isValid && (curDesp->tag.offset - thisBandOff) < thisBandSize && curDesp->tag.offset >= thisBandOff)
        {

            returnCode = DISK_READ(smr_fd, BandBuffer + (curDesp->tag.offset - thisBandOff) * BLKSIZE, BLKSIZE, curPos * BLKSIZE + OFF_PB); //ff_t offset_inband = curDesp->tag.offset - thisBandOff;

            /* clean the meta data */
            dirty_n_inBand++;
            unsigned long hash_code = ssdtableHashcode(curDesp->tag);
            ssdtableDelete(curDesp->tag, hash_code);

            int isHeadChanged = invalidDespInFIFO(curDesp); // Invalidate the current block and check if the head block get changed.
            if(isHeadChanged)
            {
                curPos = global_fifo_ctrl.head;
                continue;
            }
        }
        curPos = nextPos;
    }
    simu_n_collect_fifo += dirty_n_inBand;



    /* flush whole band to smr */
    /** W **/
    returnCode = DISK_WRITE(smr_fd, BandBuffer, thisBandSize, thisBandOff + OFF_BAND_REGION);

    simu_flush_bands++;
    simu_flush_band_size += thisBandSize;

    // wtrAmp = (double)thisBandSize / (dirty_n_inBand * BLKSIZE);
    // STT->wtrAmp_cur = wtrAmp;
    // STT->WA_sum += wtrAmp;
    // STT->n_RMW ++;
    simu_RMWs ++;

    // char log[256];
    // sprintf(log,"[Emulator]: RMW number:%lu, write amplifcation:%f\n",STT->n_RMW, wtrAmp);
    // sac_log(log, Log_emu);
    // sprintf(log,"[Emulator]: dirty blocks in band colect=%ld, bandsize=%ld\n", dirty_n_inBand, thisBandSize/BLKSIZE);
    // sac_log(log, Log_emu);
}

static unsigned long
getBandSize(uint64_t offset)
{
    uint64_t		i, size, total_size = 0;
    for (i = 0; i < band_size_num; i++)
    {
        size = BNDSZ / 2 + i * 1024000;
        if (total_size + size * num_each_size >= offset)
            return size;
        total_size += size * num_each_size;
    }
    return 0;
}
static uint64_t
getBandOffset(uint64_t blk_off)
{
    uint64_t i, size, total_size = 0;
    for (i = 0; i < band_size_num; i++)
    {
        size = BNDSZ / 2 + i * 1024000;
        if (total_size + size * num_each_size > blk_off)
            return total_size + ((blk_off - total_size) / size) * size;
        total_size += size * num_each_size;
    }
    return 0;
}

void DM_Simulator_PrintStatistic()
{
    printf("----------------DM-SMR SIMULATOR------------\n");
    printf("Block/Band Count:\n");
    printf("PB_read_blks:\t%ld\nPB_write_blks:\t%ld\nGC_collect_blks:\t%ld\nSMR_read_blks:\t%ld\nPB_write_hits:\t%ld\n",simu_n_read_fifo, simu_n_write_fifo,simu_n_collect_fifo, simu_n_read_smr, simu_n_fifo_write_HIT);
    printf("RMWs:\t%ld\nGC_bandsize(Byte):\t%ld\n", simu_flush_bands, simu_flush_band_size);
    printf("WA_avg:\t%lf\n",(float)(simu_flush_band_size / BLKSIZE) / (simu_n_write_fifo - NBLOCK_SMR_PB));
}
